/* @flow */

import config from "../config";
import Watcher from "../observer/watcher";
import Dep, { pushTarget, popTarget } from "../observer/dep";
import { isUpdatingChildComponent } from "./lifecycle";

import {
  set,
  del,
  observe,
  defineReactive,
  toggleObserving,
} from "../observer/index";

import {
  warn,
  bind,
  noop,
  hasOwn,
  hyphenate,
  isReserved,
  handleError,
  nativeWatch,
  validateProp,
  isPlainObject,
  isServerRendering,
  isReservedAttribute,
  invokeWithErrorHandling,
} from "../util/index";

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop,
};
/**
 *
 * 设置代理将key 代理到target 上
 * @param {*} target
 * @param {*} sourceKey
 * @param {*} key
 */
export function proxy(target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key];
  };
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}
/**
 *主要做了两件事
  1、数据响应式的入口：分别处理 props、methods、data、computed、watch
  2、优先级：props、methods、data、computed 对象中的属性不能重复，优先级和列出顺序一致
  其中computed中的key 不能和 props、data中的key 重复，methods不影响
 * @param {*} vm
 */
export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  //处理props 对象，为props 对象的每个属性设置响应式，并将其代理到 vm 实例上
  if (opts.props) initProps(vm, opts.props);
  //处理methods对象，校验每个属性的值是否为函数，和propss属性判重处理，最后得到vm[key]=methods[key]
  if (opts.methods) initMethods(vm, opts.methods);
  /**
   * 做了三件事
   * 1、判重处理，data属性不能与props，methods属性相同
   * 2、代理data对象上的属性到vm 实例
   * 3、为data 对象属性设置响应式
   *
   */
  if (opts.data) {
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }

  /**
   * ★★★
   * 三件事：
   * 1、为copmputed[key]创建watcher实例，默认是懒执行
   * 2、代理computed[key]到vm实例
   * 3、判重处理 computed中的属性不能与props data中的属性相同
   *
   */
  if (opts.computed) initComputed(vm, opts.computed);


  /***
   * 三件事
   * 1、处理watch对象
   * 2、为每个watch的key创建watcher实例 key与watcher实例可能是一对多的关系
   * 3、如果设置了immediate则立即执行 回调函数
   *
   *
   */
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
  /**
   *
   * 到这里我们可以看出computed 和 watch 在本质上是没有区别的 ，都是通过watcher去实现的响应式
   * 非要说有区别那就是：
   * 1、watch适用于当数据变化时执行异步或者开销较大的操作时使用，-> 需要长时间等待的操作可以使用watch
   * 2、其中可以使用异步方法，但是没有任何意义，所以 computed 更适合一些同步操作
   */
}
//处理props 对象，为props 对象的每个属性设置响应式，并将其代理到vm实例上
function initProps(vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {};
  const props = (vm._props = {});
  //缓存props key，性能优化
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = (vm.$options._propKeys = []);
  const isRoot = !vm.$parent;
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false);
  }
  //遍历props对象
  for (const key in propsOptions) {
    //缓存key
    keys.push(key);
    //获取props[key]的默认值
    const value = validateProp(key, propsOptions, propsData, vm);
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== "production") {
      const hyphenatedKey = hyphenate(key);
      if (
        isReservedAttribute(hyphenatedKey) ||
        config.isReservedAttr(hyphenatedKey)
      ) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        );
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
              `overwritten whenever the parent component re-renders. ` +
              `Instead, use a data or computed property based on the prop's ` +
              `value. Prop being mutated: "${key}"`,
            vm
          );
        }
      });
    } else {
      //为每个key设置响应式
      defineReactive(props, key, value);
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      //代理key到vm实例上
      proxy(vm, `_props`, key);
    }
  }
  toggleObserving(true);
}


/**
 * 做了三件事
 * 1、判重处理，data属性不能与props，methods属性相同
 * 2、代理data对象上的属性到vm
 * 3、为data对象上的属性设置响应式
 *
 * @param {*} vm
 */
function initData(vm: Component) {
  let data = vm.$options.data;
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
  if (!isPlainObject(data)) {
    data = {};
    process.env.NODE_ENV !== "production" &&
      warn(
        "data functions should return an object:\n" +
          "https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
        vm
      );
  }
  // proxy data on instance
  const keys = Object.keys(data);
  const props = vm.$options.props;
  const methods = vm.$options.methods;
  let i = keys.length;
  while (i--) {
    const key = keys[i];
    if (process.env.NODE_ENV !== "production") {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        );
      }
    }
    //判重，不能与props，methods属性一致
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== "production" &&
        warn(
          `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
          vm
        );
    } else if (!isReserved(key)) {
      //将data对象上的属性代理到vm实例
      proxy(vm, `_data`, key);
    }
  }
  // observe data
  //为data对象上的属性设置响应式
  observe(data, true /* asRootData */);
}

export function getData(data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget();
  try {
    return data.call(vm, vm);
  } catch (e) {
    handleError(e, vm, `data()`);
    return {};
  } finally {
    popTarget();
  }
}

const computedWatcherOptions = { lazy: true };


/***
 * 1、为copmputed[key]创建watcher实例，默认是懒执行
 * 2、代理computed[key]到vm实例
 * 3、判重 computed中的key不能与data，props中的属性重复
 */
function initComputed(vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = (vm._computedWatchers = Object.create(null));
  // computed properties are just getters during SSR
  const isSSR = isServerRendering();
 //遍历computed对象
  for (const key in computed) {
    //获取key对应的值 即getter函数
    const userDef = computed[key];
    const getter = typeof userDef === "function" ? userDef : userDef.get;
    if (process.env.NODE_ENV !== "production" && getter == null) {
      warn(`Getter is missing for computed property "${key}".`, vm);
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      //为computed属性创建watcher实例
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        //配置项computed默认是懒执行
        computedWatcherOptions
      );
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      //代理computed 对象中的属性到vm实例，这样就可以vm.computedKey来访问计算属性了
      defineComputed(vm, key, userDef);
    } else if (process.env.NODE_ENV !== "production") {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm);
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(
          `The computed property "${key}" is already defined as a prop.`,
          vm
        );
        //非生产环境的一个判重处理computed对象中的属性不能与props data 中的属性一致
      } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(
          `The computed property "${key}" is already defined as a method.`,
          vm
        );
      }
    }
  }
}
/**
 *代理computed 对象中的key 到 target 上
 * @param {*} target
 * @param {*} key
 * @param {*} userDef
 */
export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering();
  if (typeof userDef === "function") {
    //构造属性描述符get set
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef);
    sharedPropertyDefinition.set = noop;
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop;
    sharedPropertyDefinition.set = userDef.set || noop;
  }
  if (
    process.env.NODE_ENV !== "production" &&
    sharedPropertyDefinition.set === noop
  ) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      );
    };
  }
  Object.defineProperty(target, key, sharedPropertyDefinition);
}
/**
 * 返回一个函数，这个函数在访问 vm.computedKey时被执行然后返回执行结果
 * @param {*} key
 * @returns
 */
function createComputedGetter(key) {
  //computed属性值会缓存的原理也是在这里，结合watcher.dirty ,watcher.evaluate,watcher.update实现的
  return function computedGetter() {
    //拿到当前key对应的watcher
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      /**
       * 计算key对应的值，通过执行computed.key的回调函数来得到
       * watcher.dirty属性就是computed会缓存的原因
       *   <template>
         <div>{{ computedProperty }}</div>
         <div>{{ computedProperty }}</div>
       </template>
       像这种情况下，在页面的一次渲染中，两个 dom 中的 computedProperty 只有第一个
       会执行 computed.computedProperty 的回调函数计算实际的值，
       即执行 watcher.evalaute，而第二个就不走计算过程了，
       因为上一次执行 watcher.evalute 时把 watcher.dirty 置为了 false，
       待页面更新后，wathcer.update 方法会将 watcher.dirty 重新置为 true，
       供下次页面更新时重新计算 computed.key 的结果
       */
      if (watcher.dirty) {
        watcher.evaluate();
      }
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value;
    }
  };
}

function createGetterInvoker(fn) {
  return function computedGetter() {
    return fn.call(this, this);
  };
}
/**
 * 做了三件事
 * 1、校验methods[key]必须是一个函数
 * 2、判重
 *    methods不能与props中的key相同
 *    methods中的key与vue实例上已有的方法重叠，一般是一些内置方法，比如以$或者_开头的方法
 * 3、将methods[key]放到vm实例上 得到 vm[key] = methods[key]
 * @param {*} vm
 * @param {*} methods
 */
function initMethods(vm: Component, methods: Object) {
  const props = vm.$options.props;
  for (const key in methods) {
    if (process.env.NODE_ENV !== "production") {
      if (typeof methods[key] !== "function") {
        warn(
          `Method "${key}" has type "${typeof methods[
            key
          ]}" in the component definition. ` +
            `Did you reference the function correctly?`,
          vm
        );
      }
      if (props && hasOwn(props, key)) {
        warn(`Method "${key}" has already been defined as a prop.`, vm);
      }
      //校验是否  $ 或者 _  开头
      if (key in vm && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
            `Avoid defining component methods that start with _ or $.`
        );
      }
    }
    vm[key] =
      typeof methods[key] !== "function" ? noop : bind(methods[key], vm);
  }
}


/**
 *
 * 处理watcher对象的入口做了两件事
 * 1、遍历watcher对象
 * 2、创建createWatcher函数
 * 注意一下编写watch的几种形式
 * watch:{
 *    a:function(val,oldVal){},
 *    //方法名
 *    b:'doMethod',
 *    //该回调会在任何被侦听的对象的property改变时被调用，不论其被嵌套多深
 *    c:{
 *      handler:function(val,oldVal){},
 *      deep:true
 *    },
 *    // 会在侦听开始之后被立即执行
 *    d:{
 *      handler:'doMethod',
 *      immediate:true
 *    }，
 *    //你可以传入回调数组，他们会被逐一调用
 *    e:[
 *      'handle1',
 *      functin handle2(val,oldVal){},
 *      {
 *        handle:function handle3(val,oldVal){}
 *      }
 *    ],
 *    // 用来监听 vm.e.f
 *    'e.f':function(val,oldVal){}
 * }
 * @param {*} vm
 * @param {*} watch
 */
function initWatch(vm: Component, watch: Object) {
  //遍历watch对象
  for (const key in watch) {
    const handler = watch[key];
    if (Array.isArray(handler)) {
      //handle为数组，遍历获取其中每一项，然后调用createWatcher
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      createWatcher(vm, key, handler);
    }
  }
}
/**
 * 干了两件事
 *  1、兼容性处理保证handler为一个函数
 *  2、调用$watch
 * @param {*} vm
 * @param {*} expOrFn
 * @param {*} handler
 * @param {*} options
 * @returns
 */
function createWatcher(
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  //如果handler为对象则获取其中的handler选项的值
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }
  //如果handler为一个字符串，则说明是一个methods方法 获取vm[handler]
  if (typeof handler === "string") {
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options);
}

export function stateMixin(Vue: Class<Component>) {
  // flow somehow has problems with directly declared definition object
  // when using Object.defineProperty, so we have to procedurally build up
  // the object here.
  const dataDef = {};
  dataDef.get = function () {
    return this._data;
  };
  const propsDef = {};
  propsDef.get = function () {
    return this._props;
  };
  if (process.env.NODE_ENV !== "production") {
    dataDef.set = function () {
      warn(
        "Avoid replacing instance root $data. " +
          "Use nested data properties instead.",
        this
      );
    };
    propsDef.set = function () {
      warn(`$props is readonly.`, this);
    };
  }
  Object.defineProperty(Vue.prototype, "$data", dataDef);
  Object.defineProperty(Vue.prototype, "$props", propsDef);

  Vue.prototype.$set = set;
  Vue.prototype.$delete = del;
  /**
   *  创建watcher,返回unwatch 完成五件事情
   *    1、兼容处理保证cb为函数
   *    2、标示用户watcher
   *    3、创建watcher实例
   *    4、如果设置了immediate则立即执行一次cb
   *    5、返回unwatch
   *
   * @param {*} expOrFn key
   * @param {*} cb 回调函数
   * @param {*} options 配置项，用户直接调用this.$watch时可能会传一个配置项
   * @returns 返回unwatch 用于取消watch监听
   */
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this;
    //兼容性处理因为用户调用 vm.$watch时 cb可能是对象
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options);
    }
    options = options || {};
    //options.user表示用户watcher,还有渲染watcher即updateComponent方法中实例化的watcher
    options.user = true;
    //创建watcher
    const watcher = new Watcher(vm, expOrFn, cb, options);
    //如果设置了immediate立即执行一次cb
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`;
      pushTarget();
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
      popTarget();
    }
    //返回一个unwatch用于解除监听
    return function unwatchFn() {
      watcher.teardown();
    };
  };
}
